Skip to content

Fetching on Event

If you've ever built a developer portfolio for yourself, odds are you've implemented a contact form. These forms are all over the internet.

Let's see how we can implement one:

Video Summary

  • This video shows a contact form implementation. It includes two controlled inputs for email and message, but nothing is wired up yet in terms of submitting the form.
  • The strategy is to pass a function to the onSubmit attribute on the form, and it will stop the default behavior with event.preventDefault(). We'll use fetch to manage the submission ourselves.
  • To submit the request, we'll call fetch and use the supplied endpoint. Because fetch is promise-based, we'll make it an async function, so that we can await the response
  • We want to supply two options:
    • Changing the method to POST, since that's what the endpoint expects.
    • Supplying the data, the email and message
  • We need to stringify the data with JSON.stringify(), because it's impossible to send objects over the network. If we don't do this, the browser will try to stringify it for us, and it'll send the string "[object Object]".
  • We'll derive the JSON from the response with await response.json(). This is needed because not all responses include the full body right away (eg. if it's a streaming response).
  • We see that the response was successful, and it reflects the data we submitted for verification.
  • This is the fundamental strategy for doing event-based network requests, but there's still much to do in terms of UX! We'll cover that below.

In this video, we see how to send data to our backend API using fetch. We saw how to send a POST request, how to stringify the body, and how to validate that we received the correct response from the server.

But really, this implementation is not yet complete. We need to update the UI so that the user knows what's happening at all times!

Here's the sandbox from the video above:

Code Playground

import React from 'react';

const ENDPOINT =
'https://jor-test-api.vercel.app/api/contact';

function ContactForm() {
const [email, setEmail] = React.useState('');
const [message, setMessage] = React.useState('');

const id = React.useId();
const emailId = `${id}-email`;
const messageId = `${id}-message`;

async function handleSubmit(event) {
event.preventDefault();
const response = await fetch(ENDPOINT, {
method: 'POST',
body: JSON.stringify({
email,
message,
}),
});
const json = await response.json();
console.log(json);
}

return (
<form onSubmit={handleSubmit}>
<div className="row">
<label htmlFor={emailId}>Email</label>
<input
required={true}
id={emailId}
type="email"
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
</div>
<div className="row">
<label htmlFor={messageId}>Message</label>
<textarea
required={true}
id={messageId}
value={message}
onChange={(event) => {
setMessage(event.target.value);
}}
/>
</div>
<div className="button-row">
<span className="button-spacer" />
<button>Submit</button>
</div>
</form>
);
}

export default ContactForm;

Loading, success, and error statuses

When submitting network requests, we want to update the UI to indicate 3 different statuses:

  • Loading
  • Success
  • Error

In the video below, I'll show you how I'd implement these statuses, and update the UI accordingly, but I'd encourage you to give it a shot yourself, using the playground above. Feel free to structure things however you wish, updating the UI in whichever way you feel makes the most sense.

Here's how I'd approach it:

Video Summary

  • I create a status state variable with 4 possible values:
    • idle
    • loading
    • success
    • error
  • When the form is submitted, I immediately change the status to loading
  • How can we tell if the request succeeds? While it's possible to use status codes, this can be unreliable, so we'll check the JSON output. If json.ok is truthy, we'll change the status to success. Otherwise, it'll be error.
  • When the status is loading, I'll change the UI in two ways:
    1. All inputs and buttons will be disabled. This is a visual cue that things are happening, while also stopping accidental multi-submissions.
    2. I'll change the button text from "Submit" to "Submitting…". A spinner would also work well.
  • When the status is success, we have several options. We could replace the form with a success message, or we could show a small note below the form. We'll want to reset the message, if we choose to continue showing the form.
  • For the error status, we'll show a generic error message below the form.
    • It's possible to get very granular with errors, but I prefer to rely on HTML validation. We get a lot for free!
    • Server-side validation is still important, since crafty users can disable client-side validation, but if someone explicitly disables the user-friendly validation, I see no reason to provide high-quality validation from the server.

In the video above, we touch on HTML validation. You can learn more on MDN: “Constraint Validation”.

We also touched on HTTP status codes. You can learn more in the “HTTP Status Codes” primer lesson 👀.

Here's the final sandbox from the video:

Code Playground

import React from 'react';

// Remove the “?simulatedError=true” to
// stop receiving errors:
const ENDPOINT =
'https://jor-test-api.vercel.app/api/contact?simulatedError=true';

function ContactForm() {
const [email, setEmail] = React.useState('');
const [message, setMessage] = React.useState('');
// idle | loading | success | error
const [status, setStatus] = React.useState('idle');
const id = React.useId();
const emailId = `${id}-email`;
const messageId = `${id}-message`;
async function handleSubmit(event) {
event.preventDefault();

setStatus('loading');

const response = await fetch(ENDPOINT, {
method: 'POST',
body: JSON.stringify({
email,
message,
}),
});
const json = await response.json();

if (json.ok) {
setStatus('success');
setMessage('');
} else {
setStatus('error');
}
}

return (
<form onSubmit={handleSubmit}>
<div className="row">
<label htmlFor={emailId}>Email</label>
<input
required={true}
disabled={status === 'loading'}
id={emailId}
type="email"
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
</div>
<div className="row">
<label htmlFor={messageId}>Message</label>
<textarea
required={true}
disabled={status === 'loading'}
id={messageId}
value={message}
onChange={(event) => {
setMessage(event.target.value);
}}
/>
</div>
<div className="button-row">